iT邦幫忙

2023 iThome 鐵人賽

DAY 6
0

今天我們來看看 call.respondText("Hello World!") 這段函數,是怎麼協助我們建立一個回應內容的。

我們先看到

然後,我們來看看 respondText() 的實作

public suspend fun ApplicationCall.respondText(
    text: String,
    contentType: ContentType? = null,
    status: HttpStatusCode? = null,
    configure: OutgoingContent.() -> Unit = {}
) {
    val message = TextContent(text, defaultTextContentType(contentType), status).apply(configure)
    respond(message)
}

TextContent 的實作如下

/**
 * Represents a text content that could be sent
 * @property text to be sent
 */
public class TextContent(
    public val text: String,
    override val contentType: ContentType,
    override val status: HttpStatusCode? = null
) : OutgoingContent.ByteArrayContent() {
    private val bytes = text.toByteArray(contentType.charset() ?: Charsets.UTF_8)

    override val contentLength: Long
        get() = bytes.size.toLong()

    override fun bytes(): ByteArray = bytes

    override fun toString(): String = "TextContent[$contentType] \"${text.take(30)}\""
}

這個類別繼承了 OutgoingContent.ByteArrayContent()

/**
 * Variant of a [OutgoingContent] with payload represented as [ByteArray]
 */
public abstract class ByteArrayContent : OutgoingContent() {
	/**
	 * Provides [ByteArray] which engine will send to peer
	 */
	public abstract fun bytes(): ByteArray
}

接著我們看到 OutgoingContent,這個類別的內容非常多,

我們先根據其簽名和註解,推測其內容

/**
 * Information about the content to be sent to the peer, recognized by a client or server engine
 */
public sealed class OutgoingContent {

根據註解來看,看起來是專門處理輸出資訊的類別。我們對這個類別的研究先停止在此,專心於 respondText 後續的段落。

接著,我們來看 defaultTextContentType(contentType)

/**
 * Creates a default [ContentType] based on the given [contentType] and current call.
 *
 * If [contentType] is `null`, it tries to fetch an already set "Content-Type" response header.
 * If the header is not available, `text/plain` is used. If [contentType] is specified, it uses it.
 *
 * Additionally, if a content type is `Text` and a charset is not set for a content type,
 * it appends `; charset=UTF-8` to the content type.
 */
public fun ApplicationCall.defaultTextContentType(contentType: ContentType?): ContentType {
    val result = when (contentType) {
        null -> {
            val headersContentType = response.headers[HttpHeaders.ContentType]
            headersContentType?.let {
                try {
                    ContentType.parse(headersContentType)
                } catch (_: BadContentTypeFormatException) {
                    null
                }
            } ?: ContentType.Text.Plain
        }

        else -> contentType
    }

    return if (result.charset() == null && result.match(ContentType.Text.Any)) {
        result.withCharset(Charsets.UTF_8)
    } else {
        result
    }
}

這一段邏輯相對小複雜,幸好我們有註解可以協助說明這一段的目的。

If [contentType] is null, it tries to fetch an already set "Content-Type" response header.:

val result = when (contentType) {
	null -> {
            val headersContentType = response.headers[HttpHeaders.ContentType]

headersContentType 是一個 String?,所以我們要試著將 headersContentType parse 成 ContentType 類別

headersContentType?.let {
	try {
		ContentType.parse(headersContentType)
	} catch (_: BadContentTypeFormatException) {
		null
	}
}

這邊的 ?.let 是一個 Kotlin 的常用小組合:如果前面的輸入不為 null,就讓(let)輸入做 let 裡面所定義的事情,不然就往下進行輸入為 null 該做的事情。

這邊的 let 裡面試著做 parsing,如果失敗了,我們忽略收到的例外,視為 header is not available

If the header is not available, text/plain is used.

} ?: ContentType.Text.Plain

如果有定義的話:If contentType is specified, it uses it.

 else -> contentType

Additionally, if a content type is Text and a charset is not set for a content type, it appends ; charset=UTF-8 to the content type.

return if (result.charset() == null && result.match(ContentType.Text.Any)) {
	result.withCharset(Charsets.UTF_8)
} else {
	result
}

到這邊,ApplicationCall.defaultTextContentType() 的行為完成了。

接著看到輸入的 status,這是一個 HttpStatusCode

public data class HttpStatusCode(val value: Int, val description: String) : Comparable<HttpStatusCode> {

只要是網頁後端框架,基本上一定會看到 HttpStatusCode 這個物件。

雖然這段程式碼很多,不過裡面目前沒有特別吸引我們的邏輯,我們先略過不提。

我們往下繼續看 val message = TextContent(text, defaultTextContentType(contentType), status).apply(configure)

可以看到跟之前一樣的做法,用 .apply(configure) 加上新的行為。

看到這邊,我們知道了 respondText 是怎麼將輸入的字串轉換成一個 OutgoingContent 物件,並加上對應的 contentTypestatus,以及看到了 HttpStatusCode 這個只要是網頁後端框架,基本上可以說必定存在的物件。

明天我們來看看 respond(message) 這個函數,是怎麼將訊息回傳出去的。


上一篇
Day 05:路由元素的分析,看 route() 後半段的實作內容
下一篇
Day 07:call.respondText() 後段:如何使用協程善用資源
系列文
深入解析 Kotlin 專案 Ktor 的程式碼,探索 Ktor 的強大功能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言